package com.fiberhome.app.network.download;

import android.content.Context;
import android.util.Log;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import io.reactivex.Observable;
import io.reactivex.ObservableEmitter;
import io.reactivex.ObservableOnSubscribe;
import io.reactivex.ObservableSource;
import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.annotations.NonNull;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Function;
import io.reactivex.functions.Predicate;
import io.reactivex.schedulers.Schedulers;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

/**
 * based on RXAndroid + OKHttp3.0
 * Created by Deed on 2018/1/11.
 */
public final class DownloadManager {

    private static final String TAG = "com.fiberhome.app.network.download.DownloadManager";
    private Map<String, Call> downCalls;
    private Map<String,DownloadObserver> observers;
    private OkHttpClient mClient;
    private Context context;
    private static final AtomicReference<DownloadManager> INSTANCE = new AtomicReference<>();//线程安全


    /**
     * @return DownloadManager
     */
    public static DownloadManager getInstance() {
        for (; ; ) {
            DownloadManager current = INSTANCE.get();
            if (current != null) {
                return current;
            }
            current = new DownloadManager();
            if (INSTANCE.compareAndSet(null, current)) {
                return current;
            }
        }
    }

    private DownloadManager() {
        downCalls = new HashMap<>();
        observers = new HashMap<>();
        mClient = new OkHttpClient.Builder().build();
    }

    /**
     * @param context Context
     */
    public void init(@NonNull Context context) {
        this.context = context.getApplicationContext();
    }

    /**
     * @param url      String
     * @param listener DownloadListener
     */
    public void download(String url, DownloadListener listener) {
        if (context == null) {
            Log.w(TAG, "please call init(Context) first");
            return;
        }
        download(url,context.getFilesDir().getPath(),listener);

    }

    /**
     * @param url      String
     * @param destDir  String
     * @param listener DownloadListener
     */
    public void download(String url, final String destDir, DownloadListener listener) {
        Observable observable = Observable.just(url).filter(new Predicate<String>() {
            @Override
            public boolean test(@NonNull String s) throws Exception {
                return !downCalls.containsKey(s);
            }
        }).flatMap(new Function<String, ObservableSource<DownloadBean>>() {
            @Override
            public ObservableSource<DownloadBean> apply(@NonNull String s) throws Exception {
                return Observable.just(createDownloadBean(s));
            }
        }).map(new Function<DownloadBean, DownloadBean>() {
            @Override
            public DownloadBean apply(@NonNull DownloadBean o) throws Exception {
                return getRealFileName(o,destDir);
            }
        }).flatMap(new Function<DownloadBean, ObservableSource<?>>() {
            @Override
            public ObservableSource<?> apply(@NonNull DownloadBean downloadBean) throws Exception {
                return Observable.create(new DownloadSubscribe(downloadBean,destDir));
            }
        }).observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(Schedulers.io());
        DownloadObserver o = observers.get(url);
        if (o != null) {
            o.downloadListener = listener;
        } else {
            observable.subscribe(new DownloadObserver(listener, url));
        }

    }

    /**
     *
     * @param url String
     * @return DownloadBean
     */
    public DownloadBean contains(String url) {
        DownloadObserver o = observers.get(url);
        if (o != null) {
            return o.downloadBean;
        }
        return null;
    }

    /**
     *
     * @param url String
     */
    public void cancel(String url) {
        Call call = downCalls.get(url);
        if (call != null) {
            call.cancel();
        }
        downCalls.remove(url);
    }

    /**
     * create DownloadBean
     *
     * @param url String
     * @return DownloadBean
     */
    private DownloadBean createDownloadBean(String url) {
        DownloadBean downloadBean = new DownloadBean(url);
        long contentLength = getContentLength(url);
        downloadBean.totalSize = contentLength;
        String fileName = url.substring(url.lastIndexOf("/"));
        downloadBean.fileName = fileName;
        return downloadBean;
    }

    /**
     * get download length
     *
     * @param downloadUrl String
     * @return long
     */
    private long getContentLength(String downloadUrl) {
        Request request = new Request.Builder()
                .url(downloadUrl)
                .build();
        try {
            Response response = mClient.newCall(request).execute();
            if (response != null && response.isSuccessful()) {
                long contentLength = response.body().contentLength();
                response.close();
                return contentLength == 0 ? DownloadBean.SIZE_ERROR : contentLength;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return DownloadBean.SIZE_ERROR;
    }

    private DownloadBean getRealFileName(DownloadBean downloadBean,String rootDir) {
        String fileName = downloadBean.fileName;
        long downloadLength = 0, contentLength = downloadBean.totalSize;
        File file = new File(rootDir, fileName);
        if (file.exists()) {
            downloadLength = file.length();
        }
        int i = 1;
        while (downloadLength >= contentLength) {
            int dotIndex = fileName.lastIndexOf(".");
            String fileNameOther;
            if (dotIndex == -1) {
                fileNameOther = fileName + "(" + i + ")";
            } else {
                fileNameOther = fileName.substring(0, dotIndex)
                        + "(" + i + ")" + fileName.substring(dotIndex);
            }
            File newFile = new File(rootDir, fileNameOther);
            file = newFile;
            downloadLength = newFile.length();
            i++;
        }
        downloadBean.downloadedSize = downloadLength;
        downloadBean.fileName = file.getName();
        downloadBean.filePath = file;
        return downloadBean;
    }

    private class DownloadObserver implements Observer<DownloadBean> {
        Disposable d;
        DownloadBean downloadBean;
        private DownloadListener downloadListener;
        private String url;

        private DownloadObserver(DownloadListener downloadListener, String url) {
            this.downloadListener = downloadListener;
            this.url = url;
        }

        @Override
        public void onSubscribe(@NonNull Disposable d) {
            this.d = d;
            observers.put(url,this);
        }

        @Override
        public void onNext(@NonNull DownloadBean downloadBean) {
            this.downloadBean = downloadBean;
            if (this.downloadListener != null) {
                downloadListener.onUpdate(downloadBean);
            }
        }

        @Override
        public void onError(@NonNull Throwable e) {
            if (this.downloadListener != null) {
                this.downloadListener.onError(e);
            }
        }

        @Override
        public void onComplete() {
            if (this.downloadListener != null) {
                this.downloadListener.onComplete(downloadBean);
            }
            observers.remove(url);
        }
    }

    private class DownloadSubscribe implements ObservableOnSubscribe<DownloadBean> {
        private DownloadBean downloadInfo;
        private String destDir;

        /**
         *
         * @param downloadInfo DownloadBean
         * @param destDir String
         */
        DownloadSubscribe(DownloadBean downloadInfo,String destDir) {
            this.downloadInfo = downloadInfo;
            this.destDir = destDir;
        }

        @Override
        public void subscribe(ObservableEmitter<DownloadBean> e) throws Exception {
            String url = downloadInfo.url;
            long downloadLength = downloadInfo.downloadedSize;
            long contentLength = downloadInfo.totalSize;
            e.onNext(downloadInfo);

            Request request = new Request.Builder()
                    .addHeader("RANGE", "bytes=" + downloadLength + "-" + contentLength)
                    .url(url)
                    .build();
            Call call = mClient.newCall(request);
            downCalls.put(url, call);
            Response response = call.execute();

            File file = new File(destDir, downloadInfo.fileName);
            InputStream is = null;
            FileOutputStream fileOutputStream = null;
            try {
                is = response.body().byteStream();
                fileOutputStream = new FileOutputStream(file, true);
                byte[] buffer = new byte[2048];
                int len;
                while ((len = is.read(buffer)) != -1) {
                    fileOutputStream.write(buffer, 0, len);
                    downloadLength += len;
                    downloadInfo.downloadedSize = downloadLength;
                    e.onNext(downloadInfo);
                }
                fileOutputStream.flush();
                downCalls.remove(url);
            } finally {
                if (is != null) {
                    is.close();
                }
                if (fileOutputStream != null) {
                    fileOutputStream.close();
                }

            }
            e.onComplete();
        }
    }
}
